// Copyright 2013 Petar Maymounkov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ripple

import (
	"bytes"
	"circuit/kit/iomisc"
	"fmt"
	"math/rand"
	"os"
	"os/exec"
	"path"
	"sync"
	"syscall"

	"circuit/kit/rh"
	"circuit/kit/rh/ns"
)

func buildProcHelp(name string, id uint64) string {
	return fmt.Sprintf(procHelpFmt, path.Join("/", name), id)
}

//  ▒ fd stdin stdout stderr ············ Forward the process' descriptors to this console.

const procHelpFmt = `
  This is a PROC ripple, named "%s", with QID ⟨Q%016x⟩.

  SYNOPSIS

  echo hello >> stdin        # Write to the standard input of the process.
  cat stdout                 # Read the standard output of the process.
  cat stderr                 # Read the standard error of the process.
  cat wait                   # Wait for process exit and read the exit code.
  cat stat                   # Read a description of this process' status.
  echo 9 >> kill             # Send a signal to the process.
  cat help                   # Read this help screen.

` //

// Proc is ...
type Proc struct {
	help      string
	cmd       *exec.Cmd
	stdinFID  rh.FID
	stdoutFID rh.FID
	stderrFID rh.FID
	//
	waitFID rh.FID
	helpFID rh.FID
	statFID rh.FID
	killFID rh.FID
	intrFID rh.FID
	//
	ps struct {
		sync.Mutex
		*os.ProcessState
	}
}

func NewProc(name string, cmd string, arg ...string) (*Proc, rh.FID, error) {
	p := &Proc{}
	p.cmd = exec.Command(cmd, arg...)

	// stdin, stdout, stderr FIDs
	stdinFID, err := p.cmd.StdinPipe()
	if err != nil {
		return nil, nil, err
	}
	stdoutFID, err := p.cmd.StdoutPipe()
	if err != nil {
		return nil, nil, err
	}
	stderrFID, err := p.cmd.StderrPipe()
	if err != nil {
		return nil, nil, err
	}
	p.stdinFID = ns.RegulateShare(ns.NewWriterFID("stdin", stdinFID), ns.RegimeNeverClose)
	p.stdoutFID = ns.RegulateShare(ns.NewReaderFID("stdout", stdoutFID), ns.RegimeNeverClose)
	p.stderrFID = ns.RegulateShare(ns.NewReaderFID("stderr", stderrFID), ns.RegimeNeverClose)

	// Start
	if err := p.cmd.Start(); err != nil {
		return nil, nil, err
	}

	// wait FID
	ufid, iw := ns.NewPipeReaderFID("wait")
	p.waitFID = ns.RegulateOnce(ufid)
	go func() {
		if err := p.cmd.Wait(); err != nil {
			iw.Write([]byte(err.Error()))
		}
		iw.Close()
		// save process state
		p.ps.Lock()
		defer p.ps.Unlock()
		p.ps.ProcessState = p.cmd.ProcessState
	}()

	// statFID
	p.statFID = ns.RegulateFork(func() ns.UnregulatedFID {
		var w bytes.Buffer
		// Cmd
		fmt.Fprintf(&w, "Cmd: %s\n", cmd)
		// Arg
		fmt.Fprintf(&w, "Arg: ")
		for _, a := range arg {
			fmt.Fprintf(&w, "%s ", a)
		}
		w.WriteByte('\n')
		// PID
		fmt.Fprintf(&w, "PID: %d\n", p.cmd.Process.Pid)
		// Status
		fmt.Fprintf(&w, "Status: ")
		if ps, ws := p.state(); ps != nil {
			if ps.Exited() {
				fmt.Fprintf(&w, "exited %d ", ws.ExitStatus())
			}
			if ws.Stopped() {
				w.WriteString("stopped ")
			}
			if ws.Signaled() {
				w.WriteString("signaled ")
			}
			if ws.Continued() {
				w.WriteString("continued ")
			}
		} else {
			w.WriteString("running ")
		}
		w.WriteByte('\n')
		//
		r := bytes.NewReader(w.Bytes())
		//
		return ns.NewReaderFID("proc/stat", iomisc.ReaderNopCloser(r))
	})

	// kill FID
	p.killFID = NewEndpoint(func(string) {
		p.cmd.Process.Signal(os.Kill)
	})

	// intr FID
	p.intrFID = NewEndpoint(func(string) {
		p.cmd.Process.Signal(os.Interrupt)
	})

	// help FID
	p.helpFID = NewHelp(buildProcHelp(name, uint64(rand.Int63())))

	return p, ns.NewFID("proc", &procDir{p}), nil
}

func (p *Proc) state() (*os.ProcessState, syscall.WaitStatus) {
	p.ps.Lock()
	defer p.ps.Unlock()
	if ps := p.ps.ProcessState; ps != nil {
		return ps, ps.Sys().(syscall.WaitStatus)
	}
	return nil, 0
}

// procDir is an ns.InterfaceDir
type procDir struct {
	p *Proc
}

func (pd *procDir) Help() rh.FID {
	return pd.p.helpFID
}

func (pd *procDir) Stdin() rh.FID {
	return pd.p.stdinFID
}

func (pd *procDir) Stdout() rh.FID {
	return pd.p.stdoutFID
}

func (pd *procDir) Stderr() rh.FID {
	return pd.p.stderrFID
}

func (pd *procDir) Wait() rh.FID {
	return pd.p.waitFID
}

func (pd *procDir) Stat() rh.FID {
	return pd.p.statFID
}

func (pd *procDir) Kill() rh.FID {
	return pd.p.killFID
}

func (pd *procDir) Interrupt() rh.FID {
	return pd.p.intrFID
}
